msg_tool\scripts\yaneurao\itufuru/
archive.rs

1//! Yaneurao Itufuru Archive File (.scd)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::encode_string;
6use crate::utils::struct_pack::*;
7use crate::utils::xored_stream::XoredStream as Crypto;
8use anyhow::Result;
9use msg_tool_macro::*;
10use std::collections::HashMap;
11use std::io::{Read, Seek, SeekFrom, Write};
12use std::sync::{Arc, Mutex};
13
14#[derive(Debug)]
15/// Yaneurao Itufuru Archive Builder
16pub struct ItufuruArchiveBuilder {}
17
18impl ItufuruArchiveBuilder {
19    /// Creates a new instance of `ItufuruArchiveBuilder`
20    pub const fn new() -> Self {
21        ItufuruArchiveBuilder {}
22    }
23}
24
25impl ScriptBuilder for ItufuruArchiveBuilder {
26    fn default_encoding(&self) -> Encoding {
27        Encoding::Cp932
28    }
29
30    fn default_archive_encoding(&self) -> Option<Encoding> {
31        Some(Encoding::Cp932)
32    }
33
34    fn build_script(
35        &self,
36        data: Vec<u8>,
37        _filename: &str,
38        _encoding: Encoding,
39        archive_encoding: Encoding,
40        config: &ExtraConfig,
41        _archive: Option<&Box<dyn Script>>,
42    ) -> Result<Box<dyn Script + Send + Sync>> {
43        Ok(Box::new(ItufuruArchive::new(
44            MemReader::new(data),
45            archive_encoding,
46            config,
47        )?))
48    }
49
50    fn build_script_from_file(
51        &self,
52        filename: &str,
53        _encoding: Encoding,
54        archive_encoding: Encoding,
55        config: &ExtraConfig,
56        _archive: Option<&Box<dyn Script>>,
57    ) -> Result<Box<dyn Script + Send + Sync>> {
58        if filename == "-" {
59            let data = crate::utils::files::read_file(filename)?;
60            Ok(Box::new(ItufuruArchive::new(
61                MemReader::new(data),
62                archive_encoding,
63                config,
64            )?))
65        } else {
66            let f = std::fs::File::open(filename)?;
67            let reader = std::io::BufReader::new(f);
68            Ok(Box::new(ItufuruArchive::new(
69                reader,
70                archive_encoding,
71                config,
72            )?))
73        }
74    }
75
76    fn build_script_from_reader<'a>(
77        &self,
78        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
79        _filename: &str,
80        _encoding: Encoding,
81        archive_encoding: Encoding,
82        config: &ExtraConfig,
83        _archive: Option<&Box<dyn Script>>,
84    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
85        Ok(Box::new(ItufuruArchive::new(
86            reader,
87            archive_encoding,
88            config,
89        )?))
90    }
91
92    fn extensions(&self) -> &'static [&'static str] {
93        &["scd"]
94    }
95
96    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
97        if buf_len >= 4 && buf.starts_with(b"SCR\0") {
98            Some(1)
99        } else {
100            None
101        }
102    }
103
104    fn script_type(&self) -> &'static ScriptType {
105        &ScriptType::YaneuraoItufuruArc
106    }
107
108    fn is_archive(&self) -> bool {
109        true
110    }
111
112    fn create_archive(
113        &self,
114        filename: &str,
115        files: &[&str],
116        encoding: Encoding,
117        config: &ExtraConfig,
118    ) -> Result<Box<dyn Archive>> {
119        let f = std::fs::File::create(filename)?;
120        let writer = std::io::BufWriter::new(f);
121        let archive = ItufuruArchiveWriter::new(writer, files, encoding, config)?;
122        Ok(Box::new(archive))
123    }
124}
125
126#[derive(Debug, StructPack, StructUnpack)]
127struct ItufuruFileHeader {
128    #[fstring = 12]
129    file_name: String,
130    offset: u32,
131}
132
133#[derive(Debug, StructPack)]
134struct CustomHeader {
135    #[fstring = 12]
136    file_name: String,
137    offset: u32,
138    #[skip_pack]
139    size: u32,
140}
141
142struct Entry {
143    name: String,
144    data: MemReader,
145}
146
147impl Read for Entry {
148    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
149        self.data.read(buf)
150    }
151}
152
153impl ArchiveContent for Entry {
154    fn name(&self) -> &str {
155        &self.name
156    }
157
158    fn size(&self) -> Option<u64> {
159        Some(self.data.data.len() as u64)
160    }
161
162    fn script_type(&self) -> Option<&ScriptType> {
163        Some(&ScriptType::YaneuraoItufuru)
164    }
165
166    fn data(&mut self) -> Result<Vec<u8>> {
167        Ok(self.data.data.clone())
168    }
169
170    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
171        Ok(Box::new(&mut self.data))
172    }
173}
174
175#[derive(Debug)]
176/// Yaneurao Itufuru Archive Script
177pub struct ItufuruArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
178    reader: Arc<Mutex<Crypto<T>>>,
179    first_file_offset: u32,
180    files: Vec<CustomHeader>,
181    _mark: std::marker::PhantomData<&'b ()>,
182}
183
184impl<'b, T: Read + Seek + std::fmt::Debug + 'b> ItufuruArchive<'b, T> {
185    /// Creates a new `ItufuruArchive`
186    ///
187    /// * `reader` - The reader to read the archive data from
188    /// * `archive_encoding` - The encoding used for the archive
189    /// * `config` - Extra configuration options
190    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
191        let mut header = [0u8; 4];
192        reader.read_exact(&mut header)?;
193        if &header != b"SCR\0" {
194            return Err(anyhow::anyhow!("Invalid Itufuru archive header"));
195        }
196        let file_count = reader.read_u32()?;
197        let first_file_offset = reader.read_u32()?;
198        reader.read_u32()?; // Skip unused field
199        let mut reader = Crypto::new(reader, 0xA5);
200        let mut tfiles = Vec::with_capacity(file_count as usize);
201        for _ in 0..file_count {
202            let file = ItufuruFileHeader::unpack(&mut reader, false, archive_encoding, &None)?;
203            tfiles.push(file);
204        }
205        let mut files = Vec::with_capacity(tfiles.len());
206        if !tfiles.is_empty() {
207            for i in 0..tfiles.len() - 1 {
208                let file = CustomHeader {
209                    file_name: tfiles[i].file_name.clone(),
210                    offset: tfiles[i].offset,
211                    size: tfiles[i + 1].offset - tfiles[i].offset,
212                };
213                files.push(file);
214            }
215            let last_file = &tfiles[tfiles.len() - 1];
216            let file = CustomHeader {
217                file_name: last_file.file_name.clone(),
218                offset: last_file.offset,
219                size: reader.seek(SeekFrom::End(0))? as u32 - last_file.offset - first_file_offset,
220            };
221            files.push(file);
222        }
223        Ok(ItufuruArchive {
224            reader: Arc::new(Mutex::new(reader)),
225            first_file_offset,
226            files,
227            _mark: std::marker::PhantomData,
228        })
229    }
230}
231
232impl<'b, T: Read + Seek + std::fmt::Debug + 'b> Script for ItufuruArchive<'b, T> {
233    fn default_output_script_type(&self) -> OutputScriptType {
234        OutputScriptType::Json
235    }
236
237    fn default_format_type(&self) -> FormatOptions {
238        FormatOptions::None
239    }
240
241    fn is_archive(&self) -> bool {
242        true
243    }
244
245    fn iter_archive_filename<'a>(
246        &'a self,
247    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
248        Ok(Box::new(
249            self.files.iter().map(|s| Ok(s.file_name.to_owned())),
250        ))
251    }
252
253    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
254        Ok(Box::new(self.files.iter().map(|s| Ok(s.offset as u64))))
255    }
256
257    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
258        if index >= self.files.len() {
259            return Err(anyhow::anyhow!(
260                "Index out of bounds: {} (max: {})",
261                index,
262                self.files.len()
263            ));
264        }
265        let entry = &self.files[index];
266        let file_offset = entry.offset as u64;
267        match self.reader.cpeek_exact_at_vec(
268            file_offset + self.first_file_offset as u64,
269            entry.size as usize,
270        ) {
271            Ok(data) => {
272                let name = entry.file_name.clone();
273                Ok(Box::new(Entry {
274                    name,
275                    data: MemReader::new(data),
276                }))
277            }
278            Err(e) => Err(anyhow::anyhow!(
279                "Failed to read file {}: {}",
280                entry.file_name,
281                e
282            )),
283        }
284    }
285}
286
287/// Archive Writer for Itufuru Archive
288pub struct ItufuruArchiveWriter<T: Write + Seek> {
289    writer: T,
290    headers: HashMap<String, CustomHeader>,
291    first_file_offset: u32,
292    encoding: Encoding,
293}
294
295impl<T: Write + Seek> ItufuruArchiveWriter<T> {
296    /// Creates a new `ItufuruArchiveWriter`
297    ///
298    /// * `writer` - The writer to write the archive data to
299    /// * `files` - List of file names to include in the archive
300    /// * `encoding` - The encoding used for the archive
301    /// * `config` - Extra configuration options
302    pub fn new(
303        mut writer: T,
304        files: &[&str],
305        encoding: Encoding,
306        _config: &ExtraConfig,
307    ) -> Result<Self> {
308        writer.write_all(b"SCR\0")?;
309        let file_count = files.len() as u32;
310        writer.write_u32(file_count)?;
311        let first_file_offset = 0x10 + file_count * 16; // 16 bytes per file header
312        writer.write_u32(first_file_offset)?;
313        writer.write_u32(0)?; // Unused field
314        let mut headers = HashMap::new();
315        for file in files {
316            headers.insert(
317                file.to_string(),
318                CustomHeader {
319                    file_name: file.to_string(),
320                    offset: 0,
321                    size: 0,
322                },
323            );
324        }
325        let mut crypto = Crypto::new(&mut writer, 0xA5);
326        for (_, header) in headers.iter() {
327            header.pack(&mut crypto, false, encoding, &None)?;
328        }
329        Ok(ItufuruArchiveWriter {
330            writer,
331            headers,
332            first_file_offset,
333            encoding,
334        })
335    }
336}
337
338impl<T: Write + Seek> Archive for ItufuruArchiveWriter<T> {
339    fn new_file<'a>(
340        &'a mut self,
341        name: &str,
342        _size: Option<u64>,
343    ) -> Result<Box<dyn WriteSeek + 'a>> {
344        let entry = self
345            .headers
346            .get_mut(name)
347            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
348        if entry.size != 0 {
349            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
350        }
351        entry.offset = self.writer.stream_position()? as u32 - self.first_file_offset;
352        Ok(Box::new(ItufuruArchiveWriterEntry::new(
353            &mut self.writer,
354            entry,
355            self.first_file_offset,
356        )))
357    }
358    fn write_header(&mut self) -> Result<()> {
359        let mut crypto = Crypto::new(&mut self.writer, 0xA5);
360        let mut entries = self.headers.values().collect::<Vec<_>>();
361        entries.sort_by_key(|h| h.offset);
362        crypto.seek(SeekFrom::Start(16))?;
363        for entry in entries.iter() {
364            entry.pack(&mut crypto, false, self.encoding, &None)?;
365        }
366        Ok(())
367    }
368}
369
370/// File Writer for Itufuru Archive
371pub struct ItufuruArchiveWriterEntry<'a, T: Write + Seek> {
372    writer: Crypto<&'a mut T>,
373    header: &'a mut CustomHeader,
374    first_file_offset: u32,
375    pos: usize,
376}
377
378impl<'a, T: Write + Seek> ItufuruArchiveWriterEntry<'a, T> {
379    fn new(writer: &'a mut T, header: &'a mut CustomHeader, first_file_offset: u32) -> Self {
380        let writer = Crypto::new(writer, 0xA5);
381        ItufuruArchiveWriterEntry {
382            writer,
383            header,
384            first_file_offset,
385            pos: 0,
386        }
387    }
388}
389
390impl<'a, T: Write + Seek> Write for ItufuruArchiveWriterEntry<'a, T> {
391    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
392        self.writer.seek(SeekFrom::Start(
393            self.header.offset as u64 + self.first_file_offset as u64 + self.pos as u64,
394        ))?;
395        let written = self.writer.write(buf)?;
396        self.pos += written;
397        self.header.size = self.header.size.max(self.pos as u32);
398        Ok(written)
399    }
400
401    fn flush(&mut self) -> std::io::Result<()> {
402        self.writer.flush()
403    }
404}
405
406impl<'a, T: Write + Seek> Seek for ItufuruArchiveWriterEntry<'a, T> {
407    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
408        let new_pos = match pos {
409            SeekFrom::Start(offset) => offset as usize,
410            SeekFrom::End(offset) => {
411                if offset < 0 {
412                    if (-offset) as usize > self.header.size as usize {
413                        return Err(std::io::Error::new(
414                            std::io::ErrorKind::InvalidInput,
415                            "Seek from end exceeds file length",
416                        ));
417                    }
418                    self.header.size as usize - (-offset) as usize
419                } else {
420                    self.header.size as usize + offset as usize
421                }
422            }
423            SeekFrom::Current(offset) => {
424                if offset < 0 {
425                    if (-offset) as usize > self.pos {
426                        return Err(std::io::Error::new(
427                            std::io::ErrorKind::InvalidInput,
428                            "Seek from current exceeds current position",
429                        ));
430                    }
431                    self.pos.saturating_sub((-offset) as usize)
432                } else {
433                    self.pos + offset as usize
434                }
435            }
436        };
437        self.pos = new_pos;
438        Ok(self.pos as u64)
439    }
440
441    fn stream_position(&mut self) -> std::io::Result<u64> {
442        Ok(self.pos as u64)
443    }
444}